#!/usr/bin/ruby
#
# Generate initial .spec and _service files for each barclamp.
# This is most likely a one-off operation since we will probably
# want to manually tweak individual barclamp .spec files later.
#
# Arguments for having a spec file per barclamp:
#   - separate .spec    files are easier to read / edit / compare than one huge one
#   - separate _service files are easier to read / edit / compare than one huge one
#   - osc service disabledrun is much quicker and more fine-grained
#     (avoids unnecessary re-fetch of all barclamps)
#   - (re)builds of individual barclamps are much quicker and more fine-grained
#     (avoids unnecessary rebuild of all barclamps, and any consequent risk of
#      redundant package updates)
#   - separate changelog per barclamp
#
# Arguments for having a single spec file with a sub-package per barclamp:
#   - can't think of any right now

require 'rubygems'
require 'erb'
require 'yaml'
require 'kwalify'
require 'getoptlong'
require 'fileutils'

def debug(msg)
  STDERR.puts "DEBUG: " + msg unless ENV['DEBUG'].nil?
end

def die(msg)
  STDERR.puts(msg)
  exit(1)
end

class Barclamp
  attr_reader :rawname, :name, :displayname, :requires, :pkg
  @@groups = Hash.new
  def initialize(path, name, dest)
    yaml_path = File.join(path,'crowbar.yml')
    @yaml = YAML.load_file(yaml_path)

    bcy = @yaml['barclamp']

    @rawname = name
    @name = bcy['name'].tr('_','-')
    @path = path
    @dest = dest

    @displayname = bcy['display']
    if bcy['member']
      bcy['member'].each do |grp|
        debug("Adding #{@name} to group #{grp}")
        @@groups[grp] ||= Array.new
        @@groups[grp] << @name
      end
    end

    # Revisit once we get a real versioning scheme.
    @version = Time.now.utc.strftime('%Y%m%d.%H%M%S')
    @barclamp_install = "#{ENV['CROWBAR_DIR']}/extra/barclamp_install.rb"
    @pkg = "crowbar-barclamp-#@name"
  end

  def make_schemas(schemas)
    res = {}
    errs = []
    schemas.each do |s|
      schema_name = s.split('/')[-1].gsub(/\.schema$/,'')
      debug "Loading kwalify schema from #{s}"
      validator = Kwalify::MetaValidator.instance
      parser = Kwalify::Yaml::Parser.new(validator)
      begin
        document = parser.parse_file(s)
      rescue Kwalify::SyntaxError => e
        errs << "Schema #{e.to_s}"
        next
      end
      if parser.errors && ! parser.errors.empty?
        parser.errors.each do |err|
          errs << "Schema #{s} parse error: #{err.linenum}:#{err.column} [#{err.path}] #{err.message}"
        end
        next
      end
      res[schema_name] = Kwalify::Validator.new(Kwalify::Yaml.load_file(s))
    end
    [ res, errs ]
  end

  def solve_requires
    debug("Soving requirements for #{name}")
    @requires = Array.new
    bc_requires = (@yaml['barclamp']['requires'] rescue Array.new)
    if bc_requires.nil? || bc_requires.empty?
      @requires << 'crowbar-barclamp-crowbar' unless @name == "crowbar"
      return
    end
    reqs = Array.new
    bc_requires.each do |req|
      if /^@/ =~ req
        die "#{name}: invalid group #{req}" unless @@groups[req[1..-1]]
        reqs << @@groups[req[1..-1]]
      else
        reqs << req
      end
    end
    @requires = reqs.flatten.compact.sort.uniq.map{|e|
      "crowbar-barclamp-#{e}".tr('_','-')
    }
    debug("#{name} requires #{@requires.join(', ')}")
  end

  def validate_bags(schemas,bags)
    errs = []
    bags.each do |f|
      schema = f.split('/')[-1].gsub(/\.json$/,'')
      next unless schemas[schema]
      debug("Validating bag #{f} with schema #{schema}")
      parser = Kwalify::Yaml::Parser.new(schemas[schema])
      begin
        document = parser.parse_file(f)
      rescue Kwalify::SyntaxError => e
        errs << "Data bag #{e.to_s}"
        next
      end
      next unless parser.errors && !parser.errors.empty?
      parser.errors.each do |err|
        errs << "Data bag #{f} error: #{err.linenum}:#{err.column} [#{err.path}] #{err.message}"
      end
    end
    errs
  end

  def validate
    errors = []
    data_bags = File.join(@path,"chef","data_bags")
    if File.directory?(data_bags)
      schemas, errs  = make_schemas(Dir.glob(File.join(data_bags,"*","*.schema")))
      errors << errs
      # Next, use the known-validated schemas to validate the data bags.
      errors << validate_bags(schemas,Dir.glob(File.join(data_bags,"*","*.json")))
    end
    errors.flatten!
    errors.compact!
    template_name = File.join(@path,"bc-template-#{@rawname}.json")
    return errors unless File.exists?(template_name)
    schema_name = File.join(@path,"bc-template-#{@rawname}.schema")
    tmp_schema = false
    unless File.exists?(schema_name)
      # Synthesize a schema from fragments
      # The first four are static
      id_frag = %Q({ "type": "str", "required": true, "pattern": "/^bc-template-#{@rawname}/" })
      desc_frag = %Q({ "type": "str", "required": true })
      roles_frag = %Q({
    "type": "map",
    "required": false,
    "mapping": {
        = : {
            "type": "map",
            "required": true,
            "mapping": {
                "implicit": { "type": "bool", "required": false },
                "admin_implicit": { "type": "bool", "required": false },
                "jig": { "type": "str", "required": true }
            }
        }
    }
})
      deployment_frag = %Q({
    "type": "map",
    "required": true,
    "mapping": {
         "#{@rawname}": {
             "type": "map",
             "required": true,
             "mapping": {
                 "crowbar-revision": { "type": "int", "required": true },
                 "crowbar-committing": { "type": "bool" },
                 "crowbar-queued": { "type": "bool" },
                 "element_states": {
                     "type": "map",
                     "mapping": {
                             = : {
                                 "type": "seq",
                                 "required": true,
                                 "sequence": [ { "type": "str" } ]
                             }
                     }
                 },
                 "elements": {
                     "type": "map",
                     "required": true,
                     "mapping": {
                             = : {
                                 "type": "seq",
                                 "required": true,
                                 "sequence": [ { "type": "str" } ]
                             }
                     }
                 },
                 "element_order": {
                     "type": "seq",
                     "required": true,
                     "sequence": [ {
                         "type": "seq",
                         "sequence": [ { "type": "str" } ]
                     } ]
                 },
                 "config": {
                     "type": "map",
                     "required": true,
                     "mapping": {
                         "environment": { "type": "str", "required": true },
                         "mode": { "type": "str", "required": true },
                         "transitions": { "type": "bool", "required": true },
                         "transition_list": {
                             "type": "seq",
                             "required": true,
                             "sequence": [ { "type": "str" } ]
                         }
                     }
                 }
             }
         }
    }
})
      # attr_frag is the only one that is barclamp specific.
      attr_frag = File.join(@path,"attributes.schema")
      raise "Cannot find #{f}!" unless File.exists?(attr_frag)
      schema_skel = %Q(
{
    "type": "map",
    "required": true,
    "mapping": {
        "id": #{id_frag},
        "description": #{desc_frag},
        "attributes": #{IO.read(attr_frag)},
        "roles": #{roles_frag},
        "deployment": #{deployment_frag}
    }
}
)
      schema_name = File.join('','tmp',"bc-template-#{@rawname}.schema")
      debug("Validation schema for #{@rawname} written to #{schema_name}")
      tmp_schema = true
      File.open(schema_name,'w') do |f|
        f.write(schema_skel)
      end
    end
    schemas,errs = make_schemas([schema_name])
    errors << errs
    errors << validate_bags(schemas,[File.join(@path,"bc-template-#{@rawname}.json")])
    File.unlink(schema_name) if tmp_schema
    errors.flatten.compact
  end

  def make_rpm
    res = true
    FileUtils.cd(@path) do
      @requires += @yaml['rpms']['required_pkgs'] if (@yaml['rpms']['required_pkgs'] rescue nil)
      outfile="#{@pkg}.spec"
      tmpl = File.join(ENV["CROWBAR_DIR"],"packaging","rpm","crowbar-barclamp.spec.erb")
      unless File.exists?(tmpl)
        die("Cannot find the spec template at #{spec_template}")
      end
      template=ERB.new(File.read(tmpl))
      File.open(outfile.to_s, 'w') do |f|
        f.puts template.result(binding)
      end
      out = %x{rpmbuild -bb "#{outfile}" 2>&1}
      res = ($? == 0)
      die(out) unless res
    end
    return res
  end

  def make_deb
    res = true
    FileUtils.cd(@path) do
      debug("In #{Dir.pwd}")
      # All of our Debian packages need these.
      @requires += @yaml['debs']['required_pkgs'] if (@yaml['debs']['required_pkgs'] rescue nil)
      @requires << '${misc:Depends}'
      debug(@requires.inspect)
      tmpls = {}
      ["control","rules","postinst"].each do |tmpl|
        t = File.join("debian",tmpl + '.erb')
        unless File.exists?(t)
          die("Cannot find the template at #{t}")
        end
        template=ERB.new(File.read(t))
        outfile=File.join("debian",tmpl)
        File.open(outfile.to_s, 'w') do |f|
          f.puts template.result(binding)
        end
      end
      out = %x{dpkg-buildpackage -d -b -uc 2>&1}
      res = ($? == 0)
      die(out) unless res
    end
    return res
  end

  def make_tar
    res = true
    FileUtils.cd(File.join(@path,'..')) do
      res = system("tar chf - \"#{@rawname}\" |gzip -9 > \"#{@dest}/#{@name}-barclamp.tar.gz\"")
    end
    return res
  end
end

def usage()
  puts "Usage:"
  puts "#{__FILE__} [--type <tar|deb|rpm>] [--dest <dir>] /path/to/barclamp ..."
  exit 1
end

opts = GetoptLong.new(
  [ '--type', '-t', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--dest', '-d', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--help', '-h', GetoptLong::NO_ARGUMENT ]
)

@@dest = Dir.pwd
@@type = "tar"

opts.each do |opt,arg|
  case opt
  when "--help" then usage
  when "--type" then @@type = arg
  when "--dest" then @@dest = arg
  end
end

usage if ARGV.length < 1

unless ENV["CROWBAR_DIR"]
  die("CROWBAR_DIR is not in the environment.  Are you running me directly?")
end

unless File.directory?(@@dest)
  die("Destination directory #{@@dest} does not exist.")
end

barclamps = Array.new

ARGV.each do |bc_dir|
  bc_name = bc_dir.split(File::Separator)[-1]
  unless File.exists?(File.join(bc_dir,"crowbar.yml"))
    die("#{bc_dir} does not point to a barclamp")
  end
  barclamps << Barclamp.new(bc_dir, bc_name, @@dest)
end

validation_errors = []
barclamps.each do |bc|
  validation_errors << bc.validate
end

validation_errors.flatten!
validation_errors.compact!
unless validation_errors.empty?
  STDERR.puts("Validation failed!")
  validation_errors.each do |e|
    STDERR.puts(e)
  end
  exit(1)
end

barclamps.each do |bc|
  bc.solve_requires
  case @@type
  when "rpm" then bc.make_rpm
  when "deb" then bc.make_deb
  when "tar" then bc.make_tar
  else die("Unknown package type #{@@type}")
  end || die("Could not build #{@@type} for #{bc.rawname}")
  STDERR.puts("Built #{@@type} for #{bc.rawname}")
end
